using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using Subkismet.Services.GoogleSafeBrowsing.Provider;

namespace Subkismet.Services.GoogleSafeBrowsing
{
    /// <summary>
    /// Manages the process of synchronization between local data and Google server.
    /// </summary>
    internal class Synchronizer
    {
        private string apiKey;

        #region Public constructor

        /// <summary>
        /// Public constructor.
        /// </summary>
        /// <param name="apiKey">The API key for the Google Safe Browsing service.</param>
        public Synchronizer(string apiKey)
        {
            this.apiKey = apiKey;
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Updates a black list and synchronizes its local data with the Google server.
        /// </summary>
        /// <param name="listType">The black list type.</param>
        /// <returns>A Boolean value that specifies if the update process was successfull.</returns>
        public bool Update(BlackListType listType)
        {
            try
            {
                string data = GetData(listType, apiKey);

                string version = GetVersionFromText(data);

                List<Key> keys = GetKeysFromText(data);

                if (!IsUpdate(data))
                    DataProviderManager.Provider.Clear(listType);

                foreach (Key key in keys)
                {
                    if (key.Type == KeyType.Deletion)
                    {
                        DataProviderManager.Provider.RemoveKey(key.Value, listType);
                        keys.Remove(key);
                    }
                }

                DataProviderManager.Provider.AddKeys(keys, listType);

                DataProviderManager.Provider.UpdateVersion(version, listType);

                return true;
            }
            catch (Exception ex)
            {
                return false;
                throw ex;
            }
        }

        /// <summary>
        /// Verifies a list of links with local data and returns
        /// the number of the phishing or malware links.
        /// </summary>
        /// <param name="links">A list of strings for the links.</param>
        /// <param name="phishingCount">The number of the phishing links (out).</param>
        /// <param name="malwareCount">The number of the malware links (out).</param>
        public void CheckLinks(List<string> links, out int phishingCount, out int malwareCount)
        {
            List<string> linksToCheck = new List<string>();
            linksToCheck = GetLinksToCheck(links);

            int phishingResult = 0;
            int malwareResult = 0;

            List<string> phishingKeys = DataProviderManager.Provider.GetKeys(BlackListType.Phishing);
            List<string> malwareKeys = DataProviderManager.Provider.GetKeys(BlackListType.Malware);

            foreach (string link in linksToCheck)
            {
                MD5 md5 = MD5.Create();

                if (phishingKeys.Contains(link))
                    phishingResult++;
                else if (malwareKeys.Contains(link))
                    malwareResult++;
            }

            phishingCount = phishingResult;
            malwareCount = malwareResult;
        }

        #endregion

        #region Private methods

        /// <summary>
        /// Gets data from Google server for a black list.
        /// </summary>
        /// <param name="listType">The black list type.</param>
        /// <param name="apiKey">The client API key.</param>
        /// <returns>The string value of the data.</returns>
        private string GetData(BlackListType listType, string apiKey)
        {
            HttpClient client = new HttpClient();

            string url = string.Format("http://sb.google.com/safebrowsing/update?client=api&apikey={0}&version=", apiKey);
            if (listType == BlackListType.Phishing)
                url = url + "goog-black-hash:";
            else
                url = url + "goog-malware-hash:";

            string version = DataProviderManager.Provider.GetLatestVersion(listType);
            if (!string.IsNullOrEmpty(version))
                version = version.Replace(".", ":");
            else
                version = "1:-1";

            url = url + version;

            Uri uri = new Uri(url);

            string result = client.GetRequest(uri, GoogleSafeBrowsing.GetUserAgent("Subkismet"), 90000, null);

            return result;
        }

        /// <summary>
        /// Gets the version of the list from the text value of the data.
        /// </summary>
        /// <param name="data">The data as string.</param>
        /// <returns>String value of the version.</returns>
        private string GetVersionFromText(string data)
        {
            Regex regEx = new Regex(@"\[.+(\d{1,20}\.\d{1,20})\]", RegexOptions.Multiline);

            string version = string.Empty;

            if (regEx.IsMatch(data))
                version = regEx.Match(data).Groups[1].Captures[0].Value;


            return version;
        }

        /// <summary>
        /// Checks if the current response from the server is an update
        /// to the local lists or a freshh new list.
        /// </summary>
        /// <param name="data">String value of the data.</param>
        /// <returns>Boolean value that specifies if the response is an update or not.</returns>
        private bool IsUpdate(string data)
        {
            if (data.Contains("update"))
                return true;
            else
                return false;
        }

        /// <summary>
        /// Fetches the list of keys from the Google server response.
        /// </summary>
        /// <param name="data">String value of the data returned from the Google server.</param>
        /// <returns>A list of Key objects for all keys included in the response.</returns>
        private List<Key> GetKeysFromText(string data)
        {
            Regex regEx = new Regex(@"([\+|\-][a-zA-Z0-9]+)", RegexOptions.Multiline);
            MatchCollection matches = regEx.Matches(data);

            List<Key> keys = new List<Key>();

            foreach (Match match in matches)
            {
                string strKey = match.Captures[0].Value;

                if (strKey.Length > 10)
                {
                    Key tempKey = new Key();

                    if (strKey.StartsWith("+"))
                        tempKey.Type = KeyType.Addition;
                    else if (strKey.StartsWith("-"))
                        tempKey.Type = KeyType.Deletion;

                    tempKey.Value = strKey.Remove(0, 1);

                    keys.Add(tempKey);
                }
            }

            return keys;
        }

        /// <summary>
        /// Gets the MD5 hash of the string value.
        /// </summary>
        /// <param name="input">The normal input string.</param>
        /// <returns>The MD5 hash string value for the input.</returns>
        private string GetMd5Hash(string input)
        {
            MD5 md5Hasher = MD5.Create();

            byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(input));

            StringBuilder sBuilder = new StringBuilder();

            for (int i = 0; i < data.Length; i++)
            {
                sBuilder.Append(data[i].ToString("x2"));
            }

            return sBuilder.ToString();
        }

        /// <summary>
        /// Returns a list of final links to be checked with local black lists.
        /// </summary>
        /// <param name="links">List of incoming links.</param>
        /// <returns>List of links to check.</returns>
        private List<string> GetLinksToCheck(List<string> links)
        {
            List<string> finalLinks = new List<string>();

            foreach (string link in links)
            {
                Canonicalization can = new Canonicalization();
                string canonicalizaedLink = can.GetCanonicalizedUrl(link);

                Lookup lookup = new Lookup();
                finalLinks.AddRange(lookup.GetUrls(canonicalizaedLink));
            }

            return RemoveDuplicates(finalLinks);
        }

        /// <summary>
        /// Removes duplicate items from the list.
        /// </summary>
        /// <param name="results">A list of strings with duplicate items.</param>
        /// <returns>A list of strings without duplicate items.</returns>
        private List<string> RemoveDuplicates(List<string> results)
        {
            List<string> finalResults = new List<string>();

            foreach (string item in results)
            {
                if (!finalResults.Contains(item))
                    finalResults.Add(item);
            }

            return finalResults;
        }

        #endregion
    }
}
